package com.util_code.utils.javajoda;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * Date time calculations and utilities. <code>TimeUtil</code> is used by
 * {@link JDateTime} and it contains few utilities that may be used
 * elsewhere, although {@link JDateTime} is recommended for all time
 * manipulation.
 */
public class TimeUtil {

    public static final int SECONDS_IN_DAY = 60 * 60 * 24;

    public static final long MILLIS_IN_DAY = 1000L * SECONDS_IN_DAY;

    /**
     * Calculates day of year from given time stamp.
     * It may not work for some dates in 1582.
     * @return day of year in range: [1-366]
     */
    public static int dayOfYear(final int year, final int month, final int day) {
        int day_of_year;
        if (isLeapYear(year)) {
            day_of_year = ((275 * month) / 9) - ((month + 9) / 12) + day - 30;
        } else {
            day_of_year = ((275 * month) / 9) - (((month + 9) / 12) << 1) + day - 30;
        }
        return day_of_year;
    }

    /**
     * Check if the given year is leap year.
     * @return <code>true</code> if the year is a leap year
     */
    public static boolean isLeapYear(final int y) {
        boolean result = false;

        if (((y % 4) == 0) &&            // must be divisible by 4...
                ((y < 1582) ||                // and either before reform year...
                        ((y % 100) != 0) ||        // or not a century...
                        ((y % 400) == 0))) {        // or a multiple of 400...
            result = true;            // for leap year.
        }
        return result;
    }

    static final int[] MONTH_LENGTH = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    /**
     * Returns the length of the specified month in days. Month is 1 for January
     * and 12 for December.
     * @return length of the specified month in days
     */
    public static int getMonthLength(final int year, final int month) {
        return getMonthLength(year, month, isLeapYear(year));
    }

    static int getMonthLength(final int year, final int month, final boolean leap) {
        if ((month < 1) || (month > 12)) {
            throw new IllegalArgumentException("Invalid month: " + month);
        }
        if (month == 2) {
            return leap ? 29 : 28;
        }
        if ((year == 1582) && (month == 10)) {
            return 21;
        }
        return MONTH_LENGTH[month];
    }


    /**
     * Checks if date is valid.
     * @return <code>true</code> if date is valid, otherwise <code>false</code>
     */
    public static boolean isValidDate(final int year, final int month, final int day) {
        if ((month < 1) || (month > 12)) {
            return false;
        }
        int ml = getMonthLength(year, month);
        //noinspection RedundantIfStatement
        if ((day < 1) || (day > ml)) {
            return false;
        }
        return true;
    }

    /**
     * Checks if time is valid.
     * @param hour hour to check
     * @param minute minute to check
     * @param second second to check
     * @return <code>true</code> if time is valid, otherwise <code>false</code>
     */
    public static boolean isValidTime(final int hour, final int minute, final int second, final int millisecond) {
        if ((hour < 0) || (hour >= 24)) {
            return false;
        }
        if ((minute < 0) || (minute >= 60)) {
            return false;
        }
        if ((second < 0) || (second >= 60)) {
            return false;
        }
        //noinspection RedundantIfStatement
        if ((millisecond < 0) || (millisecond >= 1000)) {
            return false;
        }
        return true;
    }

    /**
     * Checks if date and time are valid.
     * @param year year to check
     * @param month month to check
     * @param day day to check
     * @param hour hour to check
     * @param minute minute to check
     * @param second second to check
     * @return <code>true</code> if date and time are valid, otherwise <code>false</code>
     */
    public static boolean isValidDateTime(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond) {
        return (isValidDate(year, month, day) && isValidTime(hour, minute, second, millisecond));
    }

    /**
     * Checks if date and time are valid.
     * @param dts date/time stamp
     * @return <code>true</code> if date and time are valid, otherwise <code>false</code>
     */
    public static boolean isValidDateTime(final DateTimeStamp dts) {
        return (isValidDate(dts.year, dts.month, dts.day) && isValidTime(dts.hour, dts.minute, dts.second, dts.millisecond));
    }

    /**
     * Calculates Astronomical Julian Date from given time stamp.
     * @return Julian Date stamp
     */
    public static JulianDateStamp toJulianDate(final DateTimeStamp time) {
        return toJulianDate(time.year, time.month, time.day, time.hour, time.minute, time.second, time.millisecond);
    }

    /**
     * Calculates Astronomical Julian Date from given time.<p>
     * <p>
     * Astronomical Julian Dates are counting from noon of the January 1st, -4712
     * (julian date 0 is -4712/01/01 12:00:00). Zero year exist. Julian Date
     * is always GMT, there are no timezones.
     * <p>
     * <p>
     * Algorithm based on Numerical Recipesin C, 2nd ed., Cambridge University
     * Press 1992, modified and enhanced by Igor Spasic.
     * @param year year
     * @param month month
     * @param day day
     * @param hour hour
     * @param minute minute
     * @param second second
     * @return julian time stamp
     */
    public static JulianDateStamp toJulianDate(int year, int month, int day, final int hour, final int minute, final int second, final int millisecond) {

        // month range fix
        if ((month > 12) || (month < -12)) {
            month--;
            int delta = month / 12;
            year += delta;
            month -= delta * 12;
            month++;
        }
        if (month < 0) {
            year--;
            month += 12;
        }

        // decimal day fraction
        double frac = (hour / 24.0) + (minute / 1440.0) + (second / 86400.0) + (millisecond / 86400000.0);
        if (frac < 0) {        // negative time fix
            int delta = ((int) (-frac)) + 1;
            frac += delta;
            day -= delta;
        }
        //double gyr = year + (0.01 * month) + (0.0001 * day) + (0.0001 * frac) + 1.0e-9;
        double gyr = year + (0.01 * month) + (0.0001 * (day + frac)) + 1.0e-9;

        // conversion factors
        int iy0;
        int im0;
        if (month <= 2) {
            iy0 = year - 1;
            im0 = month + 12;
        } else {
            iy0 = year;
            im0 = month;
        }
        int ia = iy0 / 100;
        int ib = 2 - ia + (ia >> 2);

        // calculate julian date
        int jd;
        if (year <= 0) {
            jd = (int) ((365.25 * iy0) - 0.75) + (int) (30.6001 * (im0 + 1)) + day + 1720994;
        } else {
            jd = (int) (365.25 * iy0) + (int) (30.6001 * (im0 + 1)) + day + 1720994;
        }
        if (gyr >= 1582.1015) {                        // on or after 15 October 1582
            jd += ib;
        }
        //return  jd + frac + 0.5;

        return new JulianDateStamp(jd, frac + 0.5);
    }


    /**
     * Calculates time stamp from Astronomical Julian Date.
     * @param JD julian date
     * @return time stamp
     */
    public static DateTimeStamp fromJulianDate(final double JD) {
        return fromJulianDate(new JulianDateStamp(JD));
    }


    /**
     * Calculates time stamp from Astronomical Julian Date.
     * Algorithm based on Numerical Recipesin C, 2nd ed., Cambridge University
     * Press 1992.
     * @param jds julian date stamp
     * @return time stamp
     */
    public static DateTimeStamp fromJulianDate(final JulianDateStamp jds) {
        DateTimeStamp time = new DateTimeStamp();
        int year, month, day;
        double frac;
        int jd, ka, kb, kc, kd, ke, ialp;

        //double JD = jds.doubleValue();//jdate;
        //jd = (int)(JD + 0.5);							// integer julian date
        //frac = JD + 0.5 - (double)jd + 1.0e-10;		// day fraction

        ka = (int) (jds.fraction + 0.5);
        jd = jds.integer + ka;
        frac = jds.fraction + 0.5 - ka + 1.0e-10;

        ka = jd;
        if (jd >= 2299161) {
            ialp = (int) (((double) jd - 1867216.25) / (36524.25));
            ka = jd + 1 + ialp - (ialp >> 2);
        }
        kb = ka + 1524;
        kc = (int) (((double) kb - 122.1) / 365.25);
        kd = (int) ((double) kc * 365.25);
        ke = (int) ((double) (kb - kd) / 30.6001);
        day = kb - kd - ((int) ((double) ke * 30.6001));
        if (ke > 13) {
            month = ke - 13;
        } else {
            month = ke - 1;
        }
        if ((month == 2) && (day > 28)) {
            day = 29;
        }
        if ((month == 2) && (day == 29) && (ke == 3)) {
            year = kc - 4716;
        } else if (month > 2) {
            year = kc - 4716;
        } else {
            year = kc - 4715;
        }
        time.year = year;
        time.month = month;
        time.day = day;

        // hour with minute and second included as fraction
        double d_hour = frac * 24.0;
        time.hour = (int) d_hour;                // integer hour

        // minute with second included as a fraction
        double d_minute = (d_hour - (double) time.hour) * 60.0;
        time.minute = (int) d_minute;            // integer minute

        double d_second = (d_minute - (double) time.minute) * 60.0;
        time.second = (int) d_second;            // integer seconds

        double d_millis = (d_second - (double) time.second) * 1000.0;

        // fix calculation errors
        time.millisecond = (int) (((d_millis * 10) + 0.5) / 10);

        return time;
    }

    // ---------------------------------------------------------------- gc

    /**
     * Returns Calendar month from provided JDateTime month.
     */
    public static int toCalendarMonth(final int month) {
        return month - 1;
    }

    /**
     * Returns Calendar day-of-week from provided JDateTime.
     */
    public static int toCalendarDayOfWeek(final int dayOfWeek) {
        return (dayOfWeek % 7) + 1;
    }

    public static final SimpleDateFormat HTTP_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);

    /**
     * Formats time to HTTP date/time format. Note that number of milliseconds
     * is lost.
     */
    public static String formatHttpDate(final long millis) {
        Date date = new Date(millis);
        return HTTP_DATE_FORMAT.format(date);
    }

    /**
     * Parses the HTTP date/time format. Returns <code>-1</code> if given string
     * is invalid.
     */
    public static long parseHttpTime(final String time) {
        if (time == null) {
            return -1;
        }

        try {
            return TimeUtil.HTTP_DATE_FORMAT.parse(time).getTime();
        } catch (ParseException e) {
            return -1;
        }
    }

}
